// ----------------------------------------------------------------------------
//
// Main windows service class that pulls the data from the WER Data center
// using the WER Service client API.
//
// ----------------------------------------------------------------------------
using System;
using System.Linq;
using System.Timers;
using System.ServiceProcess;
using System.Configuration;
using System.Diagnostics;
using Microsoft.WindowsErrorReporting.Services.Data.API;
using System.Collections.Generic;
using System.Text;
using System.Collections;
using System.Security;

namespace WERServices
{
    /// <summary>
    /// Class that pulls the data from the WER Data center locally
    /// using the WER Service client API.
    /// </summary>
    public class WERDataService : ServiceBase
    {
        #region Fields
        private System.Timers.Timer timer;
        private const float DEFAULT_JOB_INTERVAL_IN_HOURS = 24;
        private Proxy proxy = null;
        private HttpLiveClient liveClient = null;
        private static bool isRunning = false;
        private bool isStarted = false;
        private float jobIntervalInHours = DEFAULT_JOB_INTERVAL_IN_HOURS;
        private AlarmClock alarmClock = null;
        #endregion Fields

        #region Constructors
        /// <summary>
        /// Constructor that specified the service name and configures the events.
        /// </summary>
        public WERDataService()
        {
            //
            // set the ServiceName property.
            //
            ServiceName = "WERDataSyncService";

            //
            // configure the level of control on the service.
            //
            this.CanStop = true;
            this.CanPauseAndContinue = false;

            //
            // configure the service to write important events to the event log.
            //
            this.AutoLog = true;
        }
        #endregion Constructors

        #region Properties
        #endregion Properties

        #region Methods
        /// <summary>
        /// Methos that runs the service.
        /// </summary>
        public static void Main(string[] args)
        {
            //
            // create an instance of the WERDataService and pass it to
            // the ServiceBase.Run method if user not interactive.
            //

            WERDataService myService = new WERDataService();

            if (Environment.UserInteractive)
            {
                myService.OnStart(args);
                Console.WriteLine("Press any key to stop program");
                Console.Read();
                if (myService.isStarted)
                {
                    myService.OnStop();
                }
            }
            else
            {
                ServiceBase.Run(myService);
            }
        }

        /// <summary>
        /// Method that executes when the service starts. Instantiates the configuration values
        /// and starts the timer.
        /// </summary>
        /// <param name="args">Parameters passed to the service.</param>
        protected override void OnStart(string[] args)
        {
            //
            // Create the live id client
            //
            if (!this.TryProcessCredentials(args, out this.liveClient))
            {
                this.Stop();
                return;
            }

            //
            // parse the service interval from the app.config.
            //
            bool parseResult = float.TryParse(ConfigurationManager.AppSettings["JobIntervalInHours"]
                , out jobIntervalInHours);

            //
            // if parsing is unsuccessful then assign the default interval.
            // 
            if (parseResult == false)
            {
                jobIntervalInHours = DEFAULT_JOB_INTERVAL_IN_HOURS;
            }

            //
            // write event log entry that the service is starting.
            //
            EventLog.WriteEntry("WERDataService service is starting.");

            //
            // Set the AlarmClock with the Alarm and WakeUp event handler.
            // We will wake up at this time and start the job.
            //
            alarmClock = new AlarmClock();

            //
            // get the alarm time from the configuration file.
            //
            int hours = int.Parse(ConfigurationManager.AppSettings["Alarm.Hours"]);
            Alarm.TimeNotation timeNotation = Alarm.TimeNotation.AM;
            if (hours > 12)
            {
                hours = hours - 12;
                timeNotation = Alarm.TimeNotation.PM;
            }
            int minutes = int.Parse(ConfigurationManager.AppSettings["Alarm.Minutes"]);
            alarmClock.WakeUp += new WakeUpEventHandler(AlarmClock_WakeUp);
            alarmClock.SetAlarm(hours, minutes, timeNotation, Alarm.Repeat.Never);

            this.isStarted = true;
        }

        /// <summary>
        /// Event handler for the wake up event of the alarm clock.
        /// Start the job interval timer and also kick off the job.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">A WakeUpEventArgs object that contains the event data.</param>
        void AlarmClock_WakeUp(object sender, WakeUpEventArgs e)
        {
            //
            // set the timer and start it, so that the job will repeat after the next job interval, 
            // specified in the app.config configuration file.
            //
            timer = new Timer();
            timer.Interval = (jobIntervalInHours * 60 * 60 * 1000); // interval is in milliseconds
            timer.AutoReset = true;
            timer.Elapsed += new ElapsedEventHandler(StartSync);
            timer.Start();

            //
            // But now we have woken up, so might as well start the job.
            //
            this.SyncWerData();
        }

        /// <summary>
        /// Method that executes when the service is stopping. Releases the resources.
        /// </summary>
        protected override void OnStop()
        {
            EventLog.WriteEntry("WERDataSyncService service is stopping.");

            if (this.timer != null)
            {
                //
                // stop the timer.
                //
                timer.Stop();

                //
                // free the resources.
                //
                timer.Dispose();
                timer = null;
            }

            //
            // dispose the alarm clock resources
            //
            if (alarmClock != null)
            {
                alarmClock.Dispose();
            }

            //
            // logout from Live ID
            //
            if (null != this.liveClient)
            {
                this.liveClient.EnsureLogout();
            }

            this.isStarted = false;
        }

        /// <summary>
        /// Method to sync the WER data from the WER Service Data center using
        /// the WER Service client API.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">An ElapsedEventArgs object that contains the event data.</param>
        private void StartSync(object sender, ElapsedEventArgs e)
        {
            EventLog.WriteEntry("isRunning = " + WERDataService.isRunning.ToString());
            //
            // check if the StartSync method is still running
            // if it is then return from here.
            //
            if (WERDataService.isRunning == true)
            {
                EventLog.WriteEntry("Exiting since StartSync is running....");
                return;
            }

            //
            // Call the method that sync's wer data.
            //
            SyncWerData();
        }

        /// <summary>
        /// Method to sync the WER data from the WER Service Data center using
        /// the WER Service client API.
        /// </summary>
        private void SyncWerData()
        {
            WERDataService.isRunning = true;
            try
            {
                EventLog.WriteEntry("Called Sync Wer Data");

                //
                // create the sql server data store object to save data in sql server.
                //
                SqlServerDataStore sqlServerDataStore = new SqlServerDataStore();

                //
                // create the proxy and pass in the new data store.
                //
                proxy = new Proxy(EventLog, sqlServerDataStore);

                //
                // start the sync process with the products sync
                //
                this.SyncProducts();
            }
            catch (Exception ex)
            {
                //
                // log the exception.
                //
                this.LogException(ex);
            }
            finally
            {
                EventLog.WriteEntry("Setting WERDataService.isRunning property to false.");
                //
                // set the isRunning variable to false, so the data download can start again
                // when the timer event triggers.
                //
                WERDataService.isRunning = false;
            }
        }

        /// <summary>
        /// Method for parsing the credentials that will be used to log into the WER Service.
        /// </summary>
        private bool TryProcessCredentials(string[] args, out HttpLiveClient liveClient)
        {
            liveClient = null;

            //
            // The service requires 2 parameters - 
            // - the username 
            // - the password
            // in this order.
            //
            if (args.Length != 2)
            {
                //
                // log the error and return false;
                //
                this.LogException(
                    new ArgumentException(
                        "The service cannot start without the required 2 arguments: username and password."
                        )
                    );

                return false;
            }


            string winqualUsername = args[0];
            string winqualPassword = args[1];

            if (string.IsNullOrEmpty(winqualUsername) == true)
            {
                //
                // log the error and return false;
                //
                this.LogException(
                    new ArgumentNullException(
                        "username"
                        , "The username is either null or empty."
                        )
                    );

                return false;
            }

            if (string.IsNullOrEmpty(winqualPassword) == true)
            {
                //
                // log the error and return false;
                //
                this.LogException(
                    new ArgumentNullException(
                        "password"
                        , "The password is either null or empty."
                        )
                    );

                return false;
            }

            SecureString ss = new SecureString();
            winqualPassword.ToList().ForEach((c) => ss.AppendChar(c));

            // Attempt login the credentials to ensure they are 
            // valid at service startup. If the credentials
            // are not valid, the service will stop.
            try
            {
                liveClient = new HttpLiveClient(winqualUsername, ss);
                liveClient.EnsureLogin();
            }
            catch (Exception ex)
            {
                //
                // log the error and return false;
                //
                this.LogException(ex);
                return false;
            }

            return true;
        }

        /// <summary>
        /// Method to sync the products with the local database. This method
        /// starts the chain to save all the other objects.
        /// </summary>
        private void SyncProducts()
        {
            //
            // get the product collection.
            //
            ProductCollection productCollection = Product.GetProducts(this.liveClient);
            foreach (Product product in productCollection)
            {
                //
                // save the product locally.
                //
                proxy.SaveProduct(product);

                //
                // get the files collection for the product.
                //
                ApplicationFileCollection fileCollection = product.GetApplicationFiles(this.liveClient);

                //
                // loop through each file of the product.
                //
                foreach (ApplicationFile file in fileCollection)
                {
                    proxy.SaveFile(file, product);

                    //
                    // get the range of dates to get data for.
                    //
                    List<Log> logList = proxy.ProcessLogs(file.ID, MetaData.GetLastUpdatedDate(this.liveClient));

                    //
                    // loop through the range of dates row by row to get the events for each of those dates
                    //
                    foreach (Log log in logList)
                    {
                        //
                        // record the start time for the processing of events.
                        // This time will be logged into the Logs table in the database.
                        //
                        DateTime processingStartDateTime = DateTime.Now;

                        //
                        // get the event page reader object by pass the date range to the GetEvents method.
                        // Passing in a date range ensures that we batch process the number of dates to be processed.
                        // If we fail in the middle the next time the job runs it will start from where it left off.
                        // The event page reader will iterate over the pages of events.
                        //
                        EventPageReader eventPageReader = file.GetEvents(log.rangeStartDate, log.rangeEndDate);

                        while (eventPageReader.Read(this.liveClient) == true)
                        {
                            //
                            // get the event reader from the event page reader.
                            //
                            EventReader eventReader = eventPageReader.Events;

                            //
                            // iterate over the event reader to get the events.
                            //
                            while (eventReader.Read() == true)
                            {
                                Event fileEvent = eventReader.Event;

                                if (fileEvent != null)
                                {
                                    //
                                    // save the event to the database.
                                    //
                                    proxy.SaveEvent(fileEvent, file);

                                    //
                                    // get the list of cabs for the event.
                                    //

                                    CabCollection cabs = new CabCollection();

                                    try
                                    {
                                        cabs = fileEvent.GetCabs(this.liveClient);
                                    }
                                    catch (Exception ex)
                                    {
                                        //
                                        // log the exception and proceed
                                        //
                                        this.LogException(ex);
                                    }

                                    //
                                    // get the number of cabs to download form the configuration file
                                    //
                                    int cabsToDownloadPerEvent = 0;

                                    int.TryParse(ConfigurationManager.AppSettings["CabsToDownloadPerEvent"], out cabsToDownloadPerEvent);

                                    if (cabsToDownloadPerEvent > cabs.Count)
                                    {
                                        cabsToDownloadPerEvent = cabs.Count;
                                    }

                                    //
                                    // loop through each of the cab in the list and save the cab data
                                    // and download the cab locally.
                                    //

                                    for (int cabIndex = 0; cabIndex < cabsToDownloadPerEvent; cabIndex++)
                                    {
                                        try
                                        {
                                            //
                                            // save the cab data
                                            //
                                            proxy.SaveCab(cabs[cabIndex], fileEvent);

                                            //
                                            // download the cab to the share
                                            //
                                            cabs[cabIndex].SaveCab(
                                                ConfigurationManager.AppSettings["CabPath"]
                                                , false
                                                , this.liveClient
                                                );
                                        }
                                        catch (Exception ex)
                                        {
                                            //
                                            // log the exception and proceed
                                            //
                                            this.LogException(ex);
                                        }
                                    }

                                    //
                                    // get the event time trending data
                                    //

                                    //
                                    // get the number of days to get time trending data for
                                    //
                                    int numberOfDays = proxy.GetEventHitDays(fileEvent);

                                    //
                                    // get the time trending information
                                    //
                                    EventInfoCollection eventInfoCollection = fileEvent.GetEventDetails(numberOfDays, this.liveClient);

                                    //
                                    // loop through the event info collection and save each event info
                                    //
                                    foreach (EventInfo eventInfo in eventInfoCollection)
                                    {
                                        //
                                        // save the event info to the database
                                        //
                                        proxy.SaveEventHit(fileEvent, eventInfo);
                                    }

                                    //
                                    // commit the event info information from the staging table to the event hits table.
                                    //
                                    proxy.CommitEventHits();
                                }
                                else
                                {
                                    throw new NullReferenceException("fileEvent object is null.");
                                }
                            }
                        }

                        //
                        // record the end time for the processing of events.
                        // This time will be logged into the Logs table in the database.
                        //
                        DateTime processingEndDateTime = DateTime.Now;

                        //
                        // Save the start and end times in the Logs table in the database.
                        //
                        proxy.StampLog(log.ID, processingStartDateTime, processingEndDateTime);
                    }
                }
            }
        }

        /// <summary>
        /// Method to populate the unhandled exception in the event log.
        /// </summary>
        /// <param name="ex">Exception object to populate the log with.</param>
        private void LogException(Exception ex)
        {
            Exception baseException = ex.GetBaseException();
            StringBuilder exceptionMessage = new StringBuilder();
            exceptionMessage.AppendFormat(
                "Exception: Type - {0}, Message - {1}, Source - {2}, StackTrace - {3}, TargetSite - {4}"
                    , baseException.GetType().ToString()
                    , baseException.Message
                    , baseException.Source
                    , baseException.StackTrace
                    , baseException.TargetSite
            );

            if (baseException.Data.Count > 0)
            {
                exceptionMessage.Append(", Data - ");
                foreach (DictionaryEntry de in baseException.Data)
                {
                    exceptionMessage.AppendFormat(
                        "Key - {0}, Value - {1} :: "
                        , de.Key
                        , de.Value.ToString()
                    );
                }
            }

            EventLog.WriteEntry(
                exceptionMessage.ToString()
                , EventLogEntryType.Error
            );
        }
        #endregion Methods
    }
}